/* cal - print a calendar		Author: Maritn Minow */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>

#define	ENTRY_SIZE	3	/* 3 bytes per value		 */
#define DAYS_PER_WEEK	7	/* Sunday, etc.			 */
#define	WEEKS_PER_MONTH	6	/* Max. weeks in a month	 */
#define	MONTHS_PER_LINE	3	/* Three months across		 */
#define	MONTH_SPACE	3	/* Between each month		 */

/* Calendar() stuffs data into layout[],
 * output() copies from layout[] to outline[], (then trims blanks).
 */
char layout[MONTHS_PER_LINE][WEEKS_PER_MONTH][DAYS_PER_WEEK][ENTRY_SIZE];
char outline[(MONTHS_PER_LINE * DAYS_PER_WEEK * ENTRY_SIZE)
             + (MONTHS_PER_LINE * MONTH_SPACE) + 1];

char *weekday = "Su Mo Tu We Th Fr Sa";
char *monthname[] = { "???", /* No month 0	 */
                      "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
                      "Dec"
                    };

static void usage()
{
	fprintf(stderr, "Usage: cal [ [month] year]\n");
	exit(1);
}

/* Return day of the week for Jan 1 of the specified year. */
static int Jan1(int year)
{
	int day;

	day = year + 4 + ((year + 3) / 4); /* Julian Calendar	 */
	if (year > 1800) { /* If it's recent, do	 */
		day -= ((year - 1701) / 100); /* Clavian correction	 */
		day += ((year - 1601) / 400); /* Gregorian correction	 */
	}
	if (year > 1752) /* Adjust for Gregorian	 */
		day += 3; /* calendar		 */
	return (day % 7);
}

/* Calendar routines, intended for eventual porting to TeX
 *
 * date(year, month, week, wday)
 *	Returns the date on this week (0 is first, 5 last possible)
 *	and day of the week (Sunday == 0)
 *	Note: January is month 1.
 *
 * setmonth(year, month)
 *	Parameters are as above, sets getdate() for this month.
 *
 * int
 * getdate(week, wday)
 *	Parameters are as above, uses the data set by setmonth()
 */

/* This structure is used to pass data between setmonth() and getdate().
 * It needs considerable expansion if the Julian->Gregorian change is
 * to be extended to other countries.
 */

static struct {
	int this_month; /* month number used in 1752 checking	 */
	int feb; /* Days in February for this month	 */
	int sept; /* Days in September for this month	 */
	int days_in_month; /* Number of days in this month		 */
	int dow_first; /* Day of week of the 1st day in month	 */
} info;

static int day_month[] = { /* 30 days hath September...		 */
	0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};

/* Setup the parameters needed to compute this month
 * (stored in the info structure).
 */
void setmonth(int year, int month)
{
	int i;

	if (month < 1 || month > 12) {/* Verify caller's parameters	 */
		info.days_in_month = 0; /* Garbage flag			 */
		return;
	}
	info.this_month = month; /* used in 1752	checking	 */
	info.dow_first = Jan1(year); /* Day of January 1st for now	 */
	info.feb = 29; /* Assume leap year		 */
	info.sept = 30; /* Assume normal year		 */
	/* Determine whether it's an ordinary year, a leap year or the
	 * magical calendar switch year of 1752. */
	switch ((Jan1(year + 1) + 7 - info.dow_first) % 7) {
	case 1: /* Not a leap year		 */
		info.feb = 28;
	case 2: /* Ordinary leap year		 */
		break;

	default: /* The magical moment arrives	 */
		info.sept = 19; /* 19 days hath September	 */
		break;
	}
	info.days_in_month = (month == 2) ? info.feb :
	                     (month == 9) ? info.sept : day_month[month];
	for (i = 1; i < month; i++) {
		switch (i) { /* Special months?		 */
		case 2: /* February			 */
			info.dow_first += info.feb;
			break;

		case 9:
			info.dow_first += info.sept;
			break;

		default:
			info.dow_first += day_month[i];
			break;
		}
	}
	info.dow_first %= 7; /* Now it's Sunday to Saturday	 */
}

int getdate(int week, int wday)
{
	int today;

	/* Get a first guess at today's date and make sure it's in range. */
	today = (week * 7) + wday - info.dow_first + 1;
	if (today <= 0 || today > info.days_in_month)
		return (0);
	else if (info.sept == 19 && info.this_month == 9 && today >= 3) /* The magical month?	 */
		return (today + 11); /* If so, some dates changed	 */
	else
		return (today); /* Return the date		 */
}

/* Return the date of the month that fell on this week and weekday.
 * Return zero if it's out of range.
 */
int date(int year, int month, int week, int wday)
{
	setmonth(year, month);
	return (getdate(week, wday));
}

/* Clean up and output the text. */
static void output(int nmonths)
{
	int week;
	int month;
	char *outp;
	int i;
	char tmpbuf[21], *p;

	for (week = 0; week < WEEKS_PER_MONTH; week++) {
		outp = outline;
		for (month = 0; month < nmonths; month++) {
			/* The -1 in the following removes the unwanted
			 * leading blank from the entry for Sunday. */
			p = &layout[month][week][0][1];
			for (i = 0; i < 20; i++)
				tmpbuf[i] = *p++;
			tmpbuf[20] = 0;
			sprintf(outp, "%s   ", tmpbuf);
			outp += (DAYS_PER_WEEK * ENTRY_SIZE) + MONTH_SPACE - 1;
		}
		while (outp > outline && outp[-1] == ' ')
			outp--;
		*outp = 0;
		printf("%s\n", outline);
	}
}
/* Actually build the calendar for this month. */
void calendar(int year, int month, int indx)
{
	char *tp;
	int week;
	int wday;
	int today;

	setmonth(year, month);
	for (week = 0; week < WEEKS_PER_MONTH; week++) {
		for (wday = 0; wday < DAYS_PER_WEEK; wday++) {
			tp = &layout[indx][week][wday][0];
			*tp++ = ' ';
			today = getdate(week, wday);
			if (today <= 0) {
				*tp++ = ' ';
				*tp++ = ' ';
			} else if (today < 10) {
				*tp++ = ' ';
				*tp = (today + '0');
			} else {
				*tp++ = (today / 10) + '0';
				*tp = (today % 10) + '0';
			}
		}
	}
}

/* Print the calendar for an entire year. */
void doyear(int year)
{
	int month;

	if (year < 1 || year > 9999)
		usage();
	if (year < 100)
		printf("\n\n                                 00%2d\n\n", year);
	else
		printf("\n%35d\n\n", year);
	for (month = 1; month <= 12; month += MONTHS_PER_LINE) {
		printf("%12s%23s%23s\n", monthname[month], monthname[month + 1],
		       monthname[month + 2]);
		printf("%s   %s   %s\n", weekday, weekday, weekday);
		calendar(year, month + 0, 0);
		calendar(year, month + 1, 1);
		calendar(year, month + 2, 2);
		output(3);
	}
	printf("\n");
}

void domonth(int year, int month)
{
	if (year < 1 || year > 9999)
		usage();
	if (month <= 0 || month > 12)
		usage();
	printf("%9s%5d\n\n%s\n", monthname[month], year, weekday);
	calendar(year, month, 0);
	output(1);
	printf("\n");
}

int main(int argc, char *argv[])
{
	int arg1val;
	int arg1len;
	int arg2val;
	struct tm *tm;
	time_t now;

	if (argc <= 1) {
		now = time(NULL);
		tm = localtime(&now);
		domonth(tm->tm_year, tm->tm_mon + 1);
	} else {
		arg1val = atoi(argv[1]);
		arg1len = strlen(argv[1]);
		if (argc == 2) {
			/* Only one argument, if small, it's a month.  If
			 * large, it's a year.  Note: cal		0082	Year
			 * 0082 cal		82	Year 0082 */
			if (arg1len <= 2 && arg1val <= 12) {
				now = time(NULL);
				tm = localtime(&now);
				domonth(tm->tm_year, arg1val);
			}
			else
				doyear(arg1val);
		} else {
			/* Two arguments, allow 1980 12 or 12 1980 */
			arg2val = atoi(argv[2]);
			if (arg1len > 2)
				domonth(arg1val, arg2val);
			else
				domonth(arg2val, arg1val);
		}
	}
	return (0);
}
